package emu.decvices.cartridge.mappers;

import emu.decvices.bus.BusDevice;
import emu.decvices.bus.IRQLine;
import emu.decvices.bus.IRQLineDevice;
import emu.decvices.cartridge.Cartridge;
import emu.decvices.save.SaveHandler;


/**
 * Banks
 * CPU $6000-$7FFF：8 KB PRG RAM 库（可选）
 * CPU $8000-$9FFF（或 $C000-$DFFF）： 8 KB 可切换 PRG ROM 库
 * CPU $A000-$BFFF：8 KB 可切换 PRG ROM 库
 * CPU $C000-$DFFF（或 $8000-$9FFF）： 8 KB PRG ROM 库，固定在倒数第二个库中
 * CPU $E000-$FFFF：8 KB PRG ROM 库，固定在最后一个库中
 * PPU $0000-$07FF（或 $1000-$17FF）：2 KB 可切换 CHR 库
 * PPU $0800-$0FFF（或 $1800-$1FFF）：2 KB 可切换 CHR 库
 * PPU $1000-$13FF（或 $0000-$03FF）： 1 KB 可切换 CHR 库
 * PPU $1400-$17FF（或 $0400-$07FF）： 1 KB 可切换 CHR 库
 * PPU $1800-$1BFF（或 $0800-$0BFF）： 1 KB 可切换 CHR 库
 * PPU $1C00-$1FFF（或 $0C00-$0FFF）： 1 KB 可切换 CHR 库
 * <p>
 * 寄存器
 * MMC3 有 4 对寄存器，
 * 分别位于 $8000-$9FFF、$A000-$BFFF、$C000-$DFFF 和 $E000-$FFFF
 * - 偶数地址（$8000、$8002 等）选择低寄存器，奇数地址（$8001、$8003 等）
 * 选择每对寄存器中的高寄存器。
 * 这些可分为两个独立的功能单元：内存映射（$8000、$8001、$A000、$A001）
 * 和扫描线计数（$C000、$C001、$E000、$E001）。
 * <p>
 * Bank select ($8000-$9FFE, even)
 * 7  bit  0
 * ---- ----
 * CPMx xRRR
 * |||   |||
 * |||   +++- Specify which bank register to update on next write to Bank Data register
 * |||          000: R0: Select 2 KB CHR bank at PPU $0000-$07FF (or $1000-$17FF)
 * |||          001: R1: Select 2 KB CHR bank at PPU $0800-$0FFF (or $1800-$1FFF)
 * |||          010: R2: Select 1 KB CHR bank at PPU $1000-$13FF (or $0000-$03FF)
 * |||          011: R3: Select 1 KB CHR bank at PPU $1400-$17FF (or $0400-$07FF)
 * |||          100: R4: Select 1 KB CHR bank at PPU $1800-$1BFF (or $0800-$0BFF)
 * |||          101: R5: Select 1 KB CHR bank at PPU $1C00-$1FFF (or $0C00-$0FFF)
 * |||          110: R6: Select 8 KB PRG ROM bank at $8000-$9FFF (or $C000-$DFFF)
 * |||          111: R7: Select 8 KB PRG ROM bank at $A000-$BFFF
 * ||+------- Nothing on the MMC3, see MMC6
 * |+-------- PRG ROM bank mode (0: $8000-$9FFF swappable,
 * |                                $C000-$DFFF fixed to second-last bank;
 * |                             1: $C000-$DFFF swappable,
 * |                                $8000-$9FFF fixed to second-last bank)
 * +--------- CHR A12 inversion (0: two 2 KB banks at $0000-$0FFF,
 * four 1 KB banks at $1000-$1FFF;
 * 1: two 2 KB banks at $1000-$1FFF,
 * four 1 KB banks at $0000-$0FFF)
 * <p>
 * <p>
 * Bank select ($8000-$9FFE, even)
 * 7  bit  0
 * CPMx xRRR
 * |||   |||
 * |||   +++- 指定下一次写入bank数据寄存器时要更新哪个bank寄存器
 * |||        000: R0: 选择 PPU $0000-$07FF （或 $1000-$17FF）处的 2 KB CHR 存储体
 * |||        001: R1: 选择 PPU $0800-$0FFF （或 $1800-$1FFF）处的 2 KB CHR 存储体
 * |||        010: R2: 010: R2： 在 PPU 上选择 1 KB CHR 库，价格为 1000-13FF（或 0000-03FF）。
 * |||        011: R3: 011: R3： 在 PPU 上选择 1 KB CHR 库，价格为 1400-17FF（或 0400-07FF）。
 * |||        100: R4: 在 PPU 上选择 1 KB CHR 存储区 1800-1BFF（或 0800-0BFF）美元
 * |||        101: R5: 在 PPU 上选择 1 KB CHR 存储区 $1C00-$1FFF（或 $0C00-$0FFF）。
 * |||        110: R6: 选择 8 KB PRG ROM 库，位置为$8000-$9FFF（或$C000-$DFFF）
 * |||        111: R7: 在 $A000-$BFFF 处选择 8 KB PRG ROM 存储体
 * ||+------- MMC3 上没有任何内容，请参见 MMC6
 * |+-------- PRG ROM 库模式（0：$8000-$9FFF 可交换、
 * |                           $C000-$DFFF 固定在倒数第二组；
 * |                        1: $C000-$DFFF 可交换、
 * |                           $8000-$9FFF固定为倒数第二组)
 * +--------- CHR A12 反转（0：两个 2 KB 存储体，位置为 $0000-$0FFF
 * 4 个 1 KB 存储体，$1000-$1FFF；
 * 1: 两个 2 KB 存储体，位置为 $1000-$1FFF、
 * 4 个 1 KB 存储体，位置为 $0000-$0FFF)
 *
 * <p>
 * CHR Banks
 * CHR map mode →	$8000.D7 = 0	$8000.D7 = 1
 * PPU Bank	        Value_of_MMC3_register
 * $0000-$03FF	    R0	            R2
 * $0400-$07FF	    R3
 * $0800-$0BFF	    R1	            R4
 * $0C00-$0FFF	    R5
 * $1000-$13FF	    R2	            R0
 * $1400-$17FF	    R3
 * $1800-$1BFF	    R4	            R1
 * $1C00-$1FFF	    R5
 * 2KB 存储体只能选择偶数 CHR 存储体。(最低位将被忽略）。
 */
public class Mapper004 extends Mapper {
    private static final int PRG_BANK_LEN = 0x2000;//8kb一个块
    private static final int CHR_BANK_LEN = 0x0400;//1kb一个块
    /*
        切换prgBank时，实际mapper允许的值为6bit表示的块:111111 最大值为64，每个块8kb，因此最多512kb的prg
        但是很多游戏，比如忍者神龟prg只有256kb。但是该游戏切换prgBank时给的值可能超过32，比如它会要求切换到
        第56个块，但是该游戏实际没有这么多块。对此，要使用掩码在0-32之间旋转。
        所以prgBankMask的值为实际的块(比如32)-1的值，用于和给定的prgBank进行按位&,达到取模的目的，避免读取
        数据溢出。坑爹的问题，卡了我好长时间。。。
     */
    private int prgBankMask;//prg块掩码
    // MMC3 寄存器
    /*
    CHR A12 inversion (0: two 2 KB banks at $0000-$0FFF,
                                 four 1 KB banks at $1000-$1FFF;
                       1: two 2 KB banks at $1000-$1FFF,
                                 four 1 KB banks at $0000-$0FFF)
     */
    private int chrInversion;
    /*
    PRG ROM bank mode (0: $8000-$9FFF swappable,
                                $C000-$DFFF fixed to second-last bank;
                             1: $C000-$DFFF swappable,
                                $8000-$9FFF fixed to second-last bank)
     */
    private int prgRomBankMode;
    /*
    Bank select ($8000-$9FFE, even)
    7  bit  0
    CPMx xRRR
    |||   |||
    |||   +++- 指定下一次写入bank数据寄存器时要更新哪个bank寄存器
    |||        000: R0: 选择 PPU $0000-$07FF （或 $1000-$17FF）处的 2 KB CHR 存储体
    |||        001: R1: 选择 PPU $0800-$0FFF （或 $1800-$1FFF）处的 2 KB CHR 存储体
    |||        010: R2: 在 PPU 上选择 1 KB CHR 库，价格为 1000-13FF（或 0000-03FF）。
    |||        011: R3: 在 PPU 上选择 1 KB CHR 库，价格为 1400-17FF（或 0400-07FF）。
    |||        100: R4: 在 PPU 上选择 1 KB CHR 存储区 1800-1BFF（或 0800-0BFF）美元
    |||        101: R5: 在 PPU 上选择 1 KB CHR 库，价格 1C00-1FFF（或 0C00-0FFF）。
    |||        110: R6: 选择 8 KB PRG ROM 库，位置为$8000-$9FFF（或$C000-$DFFF）
    |||        111: R7: 在 $A000-$BFFF 处选择 8 KB PRG ROM 存储体
     */
    private int bankSelectIndex = 0;//对应bankSelect数组下标

    /**
     * +---------------+--------------+--------------+
     * | CHR map mode →|$8000.D7 = 0  | $8000.D7 = 1 |
     * |---------------+--------------+--------------|
     * |PPU Bank	   |Value of MMC3 register       |
     * |---------------+--------------+--------------|
     * |$0000-$03FF	   |R0	          | R2           |
     * |---------------|              |--------------|
     * |$0400-$07FF	   |              | R3           |
     * |---------------+--------------+--------------|
     * |$0800-$0BFF	   |R1	          | R4           |
     * |---------------|              |--------------|
     * |$0C00-$0FFF	   |              | R5           |
     * |---------------+--------------+--------------|
     * |$1000-$13FF	   |R2	          | R0           |
     * |---------------+--------------|              |
     * |$1400-$17FF	   |R3            |              |
     * |---------------+--------------+--------------|
     * |$1800-$1BFF	   |R4	          | R1           |
     * |---------------+--------------|              |
     * |$1C00-$1FFF	   |R5            |              |
     * +---------------------------------------------+
     * <p>
     * PRG map mode →	$8000.D6 = 0	$8000.D6 = 1
     * * CPU Bank	    Value of MMC3 register
     * * $8000-$9FFF	    R6	            (-2)
     * * $A000-$BFFF	    R7	            R7
     * * $C000-$DFFF	    (-2)	        R6
     * * $E000-$FFFF	    (-1)	        (-1)
     * <p>
     * <p>
     * bankIndex[0]: CHR map mode 0 或  PRG map mode 0
     * bankIndex[1]: CHR map mode 1 或  PRG map mode 1
     * bankIndex[][x]: 对应bankData中每个块应该的位置
     */
    public int bankIndex[][] = new int[][]{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, {4, 5, 6, 7, 0, 1, 2, 3, 10, 9, 8, 11}};
    ;
    /**
     * 下标             page
     * 0               R0     前1KB
     * 1               R0     后1KB
     * 2               R1     前1KB
     * 3               R1     后1KB
     * 4               R2
     * 5               R3
     * 6               R4
     * 7               R5
     * 8               R6
     * 9               R7
     * 10              倒数第二个块开始位置
     * 11              倒数第一个块的开始位置
     */
    private int[] bankData = new int[12];

    private boolean WRAMFlag;//是否允许WRAM($6000-$7FFF)
    private boolean WRAMReadOnly;

    private int irqReload;
    private int irqCounter;

    private CpuDevice cpuDevice = new CpuDevice();
    private PpuDevice ppuDevice = new PpuDevice();

    private byte[] wram = new byte[0x2000];//8kb wram

    private IRQLineDevice irqDevice = new IRQLineDevice();//产生IRQ的中断设备

    public Mapper004(Cartridge cartridge) {
        super(cartridge);
        bankData[11] = prgData.length - PRG_BANK_LEN;//倒数第一个块的开始地址
        bankData[10] = bankData[11] - PRG_BANK_LEN;//倒数第二个块的开始地址
        initWRAM();
        IRQLine.getInstance().addDevice(irqDevice);
        //prgBank块数是以16kb为单位定义在rom文件中的，由于切换是按照8kb切换，因此块数的掩码要乘以2，所以左移1位
        prgBankMask = romHeader.getPrgRomChunks()<=0x3F?(romHeader.getPrgRomChunks()<<1)-1:0x3FF;
    }

    public void initWRAM() {
        if(romHeader.isSramEnable()) {
            //读取存档
            byte[] data = SaveHandler.load(cartridge.getRomFileName());
            if (data.length > 0) {
                wram = data;
            }
        }
    }

    public BusDevice getCpuDevice() {
        return cpuDevice;
    }

    @Override
    public BusDevice getPpuDevice() {
        return ppuDevice;
    }

    private class PpuDevice extends BusDevice {
        @Override
        public boolean toDeviceWrite(int address, int data) {
            if (address >= 0x0000 && address <= 0x1FFF) {
                chrData[address] = (byte) (data & 0xFF);
                return true;
            }
            return false;
        }

        @Override
        public int fromDeviceRead(int address) {
            if (address >= 0x1C00 && address <= 0x1FFF) {
                return chrData[bankData[bankIndex[chrInversion][7]] + (address & 0x03FF)] & 0xFF;
            } else if (address >= 0x1800 && address <= 0x1BFF) {
                return chrData[bankData[bankIndex[chrInversion][6]] + (address & 0x03FF)] & 0xFF;
            } else if (address >= 0x1400 && address <= 0x17FF) {
                return chrData[bankData[bankIndex[chrInversion][5]] + (address & 0x03FF)] & 0xFF;
            } else if (address >= 0x1000 && address <= 0x13FF) {
                return chrData[bankData[bankIndex[chrInversion][4]] + (address & 0x03FF)] & 0xFF;
            } else if (address >= 0x0C00 && address <= 0x0FFF) {
                return chrData[bankData[bankIndex[chrInversion][3]] + (address & 0x03FF)] & 0xFF;
            } else if (address >= 0x0800 && address <= 0x0BFF) {
                return chrData[bankData[bankIndex[chrInversion][2]] + (address & 0x03FF)] & 0xFF;
            } else if (address >= 0x0400 && address <= 0x07FF) {
                return chrData[bankData[bankIndex[chrInversion][1]] + (address & 0x03FF)] & 0xFF;
            } else if (address >= 0x0000 && address <= 0x03FF) {
                return chrData[bankData[bankIndex[chrInversion][0]] + (address & 0x03FF)] & 0xFF;
            }
            return -1;
        }
    }

    private class CpuDevice extends BusDevice {
        @Override
        public int fromDeviceRead(int address) {
            if (WRAMFlag && address >= 0x6000 && address <= 0x7FFF) {//读取WRAM
                return wram[address & 0x1FFF] & 0xFF;
            }
            if (address >= 0x8000) {
                //CPU可寻址prgRom的总线地址范围
                if (address <= 0x9FFF) {
                    return prgData[bankData[bankIndex[prgRomBankMode][8]] + (address & 0x1FFF)] & 0xFF;
                } else if (address <= 0xBFFF) {
                    return prgData[bankData[bankIndex[prgRomBankMode][9]] + (address & 0x1FFF)] & 0xFF;
                } else if (address <= 0xDFFF) {
                    return prgData[bankData[bankIndex[prgRomBankMode][10]] + (address & 0x1FFF)] & 0xFF;
                } else {
                    return prgData[bankData[bankIndex[prgRomBankMode][11]] + (address & 0x1FFF)] & 0xFF;
                }
            }
            return -1;
        }

        @Override
        public boolean toDeviceWrite(int address, int data) {
            //写入wram(存档)
            if (WRAMFlag && !WRAMReadOnly & address >= 0x6000 && address <= 0x7FFF) {
                wram[address & 0x1FFF] = (byte) data;
                if(romHeader.isSramEnable()) {
                    SaveHandler.save(cartridge.getRomFileName(), wram);//存档
                }
                return true;
            }
            if (address >= 0x8000 && address <= 0x9FFF) {
                if ((address & 0x01) == 1) {//奇数地址
                    switch (bankSelectIndex) {
                        case 0:
                        case 1:
                            bankData[bankSelectIndex * 2] = (data & 0xFE) * CHR_BANK_LEN;
                            bankData[bankSelectIndex * 2 + 1] = bankData[bankSelectIndex * 2] + CHR_BANK_LEN;
                            break;
                        case 2:
                        case 3:
                        case 4:
                        case 5:
                            bankData[bankSelectIndex + 2] = data * CHR_BANK_LEN;
                            break;
                        case 6:
                        case 7:
                            bankData[bankSelectIndex + 2] = (data & prgBankMask) * PRG_BANK_LEN;
                            break;
                    }
                } else {//偶数地址
                    chrInversion = (data & 0x80) >> 7;
                    prgRomBankMode = (data & 0x40) >> 6;
                    bankSelectIndex = data & 0x07;
                }
                return true;
            }
            if (address >= 0xA000 && address <= 0xBFFF) {
                if ((address & 0x01) == 1) {//奇数地址
                    WRAMFlag = (data & 0x80) > 0;//bit7 使能WRAM
                    WRAMReadOnly = (data & 0x40) > 0;

                } else {//偶数地址 这个寄存器的 bit0 为 0 的话表示 垂直镜像，为 1 的话表示 水平镜像
                    setMirror((data & 0x01) == 0 ? Cartridge.Mirror.VERTICAL : Cartridge.Mirror.HORIZONTAL);
                }
                return true;
            }
            if (address >= 0xC000 && address <= 0xDFFF) {
                if ((address & 0x01) == 1) {//奇数地址
                    irqCounter = 0;
                } else {
                    irqReload = data;
                }
                return true;
            }
            if (address >= 0xE000 && address <= 0xFFFF) {
                if ((address & 0x01) == 1) {//奇数地址
                    irqDevice.irqEnable();
                } else {
                    irqDevice.irqDisable();
                    irqDevice.acknowledge();
                }
                return true;
            }
            return false;
        }
    }
    @Override
    public void onScanLine() {
        if (irqCounter == 0) {
            irqCounter = irqReload;
        } else {
            irqCounter--;
        }
        if (irqCounter == 0) {
            irqDevice.irq();
        }
    }
}
